﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using I = IML.Shared;

namespace IML
{
    public class ImageCompiler
    {
        private Stack<ActivationRecord> stack = new Stack<ActivationRecord>();
        public void DrawCanvas(I.ImageCompilerRequest request, Graphics g)
        {
            stack.Clear();
            var rect = new RectangleF(0,0, request.CanvasWidth, request.CanvasHeight);
            ActivationRecord context = new ActivationRecord(null, "Canvas");
            context.CurrentBrush = request.InitialBrush == null ? Brushes.Black : request.InitialBrush.ToBrush();
            context.AvailableSpace = rect;
            context.Graphics = g;

            g.FillRectangle(context.CurrentBrush, rect);
            stack.Push(context);
            DrawChildShapes(request, context);
            stack.Pop();

        }

        private void DrawShape(I.NestedShape shape, ActivationRecord context, PointF? AbsoluteCenter)
        {
            if (shape is I.Polygon) { context.AvailableSpace = DrawPolygon(shape as I.Polygon, context.Parent, AbsoluteCenter); }
            else { context.AvailableSpace = DrawEllipse(shape as I.Ellipse, context.Parent, AbsoluteCenter); }

            if (context.AvailableSpace.Width >4 && context.AvailableSpace.Height > 4)
            {
                stack.Push(context);
                DrawChildShapes(shape, context);
                stack.Pop();
            }
        }

        private void DrawChildShapes(I.NestedShape parent, ActivationRecord context)
        {

            var encounteredVariableNames = new HashSet<string>();
            foreach (var item in parent.ShapeDeclarations)
            {
                if (encounteredVariableNames.Contains(item.Key)) { Error("duplicate variable in scope: " + item.Key); }
                encounteredVariableNames.Add(item.Key);
                context.Variables[item.Key] = item.Value;
            }

            int invokeCnt = 1;
            foreach (var item in parent.Invocations)
            {
                if (item is I.BrushInvocation)
                {
                    context.CurrentBrush = (item as I.BrushInvocation).Brush.ToBrush();
                }
                else if (item is I.ShapeInvocation)
                {
                    var shape = (item as I.ShapeInvocation).Shape;  
                    DrawShape(shape, new ActivationRecord(context, String.Format("{0}[{1}]", shape, invokeCnt)), null);
                }
                else if (item is I.ShapeVariableInvocation)
                {
                    var varInvoke = item as I.ShapeVariableInvocation;
                    if (!context.Variables.ContainsKey(varInvoke.VariableName)) { Error("undeclared variable: " + varInvoke.VariableName); }
                    if (item is I.Repeater)
                    { ProcessReapeater(item as I.Repeater, context); }
                    
                    else
                    {
                        var childContext = new ActivationRecord(context, String.Format("(2){0}[{1}]", context.Variables[varInvoke.VariableName], invokeCnt, varInvoke.VariableName));
                        if (childContext.VariableRecursionContexts.ContainsKey(varInvoke.VariableName))
                        {
                            var recursionToken = childContext.VariableRecursionContexts[varInvoke.VariableName];
                            if (recursionToken.DeclaredDepth == 0)
                            {
                                DrawShape(context.Variables[varInvoke.VariableName], childContext, null);
                            }
                            else if (recursionToken.CurrentDepth > 0)
                            {
                                recursionToken.CurrentDepth--;
                                DrawShape(context.Variables[varInvoke.VariableName], childContext, null);
                            }
                        }
                        else
                        {
                            childContext.VariableRecursionContexts.Add(varInvoke.VariableName, new ActivationRecord.RecursionToken(varInvoke.recusionDepth));
                            DrawShape(context.Variables[varInvoke.VariableName], childContext, null);
                        }
                    }
                }
                invokeCnt++;
            }
        }

        private void ProcessReapeater(I.Repeater repeater, ActivationRecord context)
        {
            var absoluteCenters = GetAbsoluteCoordinates(repeater.PointFunction, context.AvailableSpace);
            for (int i = 0; i < absoluteCenters.Length; i++)
            {
                var childContext = new ActivationRecord(context, repeater.VariableName + "*" + i.ToString());
                if (childContext.VariableRecursionContexts.ContainsKey(repeater.VariableName))
                {
                    var recursionToken = childContext.VariableRecursionContexts[repeater.VariableName];
                    if (recursionToken.DeclaredDepth == 0)
                    {
                        DrawShape(context.Variables[repeater.VariableName], childContext, absoluteCenters[i]);
                    }
                    else if (recursionToken.CurrentDepth > 0)
                    {
                        recursionToken.CurrentDepth--;
                        DrawShape(context.Variables[repeater.VariableName], childContext, absoluteCenters[i]);
                    }
                }
                else
                {
                    childContext.VariableRecursionContexts.Add(repeater.VariableName, new ActivationRecord.RecursionToken(repeater.recusionDepth));
                    DrawShape(context.Variables[repeater.VariableName], childContext, absoluteCenters[i]);
                }
            }
        }

        private RectangleF DrawPolygon(I.Polygon polygon, ActivationRecord context, PointF? AbsoluteCenter)
        {
            float minX = float.MaxValue;
            float minY = float.MaxValue;
            float maxX = float.MinValue;
            float maxY = float.MinValue;
            if (polygon.PointsFunction != null)
            {
                PointF[] verticies = GetAbsoluteCoordinates(polygon.PointsFunction, context.AvailableSpace);
                if (verticies.Length < 3) { Error("Not enough points to draw polygon... at least 3 required"); }
                for (int i = 0; i < verticies.Length; i++)
                {
                    minX = Math.Min(minX, verticies[i].X);
                    minY = Math.Min(minY, verticies[i].Y);
                    maxX = Math.Max(maxX, verticies[i].X);
                    maxY = Math.Max(maxY, verticies[i].Y);
                }
                if (AbsoluteCenter.HasValue)
                {
                    PointF centerOffset = new PointF(minX + (maxX - minX) / 2 - AbsoluteCenter.Value.X, minY + (maxY - minY) / 2 - AbsoluteCenter.Value.Y);
                
                    for (int i = 0; i < verticies.Length; i++)
                    {
                        verticies[i].X -= centerOffset.X;
                        verticies[i].Y -= centerOffset.Y;
                    }
                    minX -= centerOffset.X;
                    maxX -= centerOffset.X;
                    minY -= centerOffset.Y;
                    maxY -= centerOffset.Y;
                }

                context.Graphics.FillPolygon(context.CurrentBrush, verticies);
#if DEBUG
                foreach (var item in verticies)
                {
                    context.Graphics.DrawEllipse(Pens.Pink, item.X-3,item.Y-3, 6,6);
                }
#endif
            }
#if DEBUG
            context.Graphics.DrawRectangle(Pens.Pink, minX, minY, maxX -minX, maxY - minY);
#endif
            return new RectangleF(minX, minY, maxX - minX, maxY - minY);
        }

        private RectangleF DrawEllipse(I.Ellipse ellipse, ActivationRecord context, PointF? AbsoluteCenter)
        {
            var parentSpace = context.AvailableSpace;
            float minDemension = Math.Min(parentSpace.Height, parentSpace.Width);
            PointF absoluteCenter = AbsoluteCenter.HasValue ? AbsoluteCenter.Value : new PointF(
                        ellipse.Center.X * (ellipse.Center.MaintainAspectRatio ? minDemension : parentSpace.Width) + parentSpace.X,
                        ellipse.Center.Y * (ellipse.Center.MaintainAspectRatio ? minDemension : parentSpace.Height) + parentSpace.Y);
                        float xDelta  = ellipse.XYRadius.X * (ellipse.XYRadius.MaintainAspectRatio ? minDemension : parentSpace.Width);
            float yDelta = ellipse.XYRadius.Y * (ellipse.XYRadius.MaintainAspectRatio ? minDemension : parentSpace.Height);
            var rect = new RectangleF(absoluteCenter.X - xDelta, absoluteCenter.Y - yDelta, xDelta * 2, yDelta * 2);

            context.Graphics.FillEllipse(context.CurrentBrush, rect);
#if DEBUG
            context.Graphics.DrawRectangle(Pens.Pink, rect.X, rect.Y, rect.Width, rect.Height);
#endif
            return rect;
        }

        private PointF[] GetAbsoluteCoordinates(I.PointProducerFunction ppFunc, RectangleF availableSpace)
        {
            float minDemension = Math.Min(availableSpace.Height, availableSpace.Width);
            if (ppFunc is I.PointList)
            {
                var pointList = ppFunc as I.PointList;
                var absolutePoints = new PointF[pointList.Verticies.Length];
                for (int i = 0; i < absolutePoints.Length; i++)
                {
                    var relPoint = pointList.Verticies[i];
                    absolutePoints[i] = new PointF(relPoint.X * (relPoint.MaintainAspectRatio ? minDemension : availableSpace.Width) + availableSpace.X,
                                                   relPoint.Y * (relPoint.MaintainAspectRatio ? minDemension : availableSpace.Height) + availableSpace.Y);
                }
                return absolutePoints;
            }

            else if (ppFunc is I.EllipseFunction)
            {
                var ppEllipses = ppFunc as I.EllipseFunction;
                var absolutePoints = new PointF[ppEllipses.AbsolutePositions.Count];
                PointF[] absoluteRadiuses = new PointF[ppEllipses.Radiuses.Length];
                PointF absoluteCenter = new PointF(
                         ppEllipses.Center.X * (ppEllipses.Center.MaintainAspectRatio ? minDemension : availableSpace.Width) + availableSpace.X,
                         ppEllipses.Center.Y * (ppEllipses.Center.MaintainAspectRatio ? minDemension : availableSpace.Height) + availableSpace.Y);

                for (int i = 0; i < ppEllipses.Radiuses.Length; i++)
                {
                    absoluteRadiuses[i] = new PointF(
                                    ppEllipses.Radiuses[i].X * (ppEllipses.Radiuses[i].MaintainAspectRatio ? minDemension : availableSpace.Width),
                                    ppEllipses.Radiuses[i].Y * (ppEllipses.Radiuses[i].MaintainAspectRatio ? minDemension : availableSpace.Height));
                }

                for (int i = 0; i < ppEllipses.AbsolutePositions.Count; i++)
                {
                    var radiusToUse = absoluteRadiuses[i % ppEllipses.Radiuses.Length];
                    absolutePoints[i] = new PointF(absoluteCenter.X + radiusToUse.X * (float)Math.Cos(ppEllipses.AbsolutePositions[i] * 2 * Math.PI),
                                                   absoluteCenter.Y + radiusToUse.Y * (float)Math.Sin(ppEllipses.AbsolutePositions[i] * 2 * Math.PI));
                }

                return absolutePoints;
            }
            else //Line function
            {
                var ppLine = ppFunc as I.LineFunction;
                var absolutePoints = new PointF[ppLine.AbsolutePositions.Count];
                PointF absoluteStart = new PointF(
                    ppLine.Start.X * (ppLine.Start.MaintainAspectRatio ? minDemension : availableSpace.Width) + availableSpace.X,
                    ppLine.Start.Y * (ppLine.Start.MaintainAspectRatio ? minDemension : availableSpace.Height) + availableSpace.Y);
                PointF absoluteEnd = new PointF(
                    ppLine.End.X * (ppLine.End.MaintainAspectRatio ? minDemension : availableSpace.Width) + availableSpace.X,
                    ppLine.End.Y * (ppLine.End.MaintainAspectRatio ? minDemension : availableSpace.Height) + availableSpace.Y);

                for (int i = 0; i < ppLine.AbsolutePositions.Count; i++)
                {
                    absolutePoints[i] = new PointF((absoluteEnd.X - absoluteStart.X) * ppLine.AbsolutePositions[i] + absoluteStart.X,
                                                    (absoluteEnd.Y - absoluteStart.Y) * ppLine.AbsolutePositions[i] + absoluteStart.Y);
                }
                return absolutePoints;
            }

            
        }


        private void Error(string error)
        {
            var sb = new StringBuilder().AppendLine(error).AppendLine().AppendLine("Stack Trace:");

            while (stack.Count>0)
            {
                sb.AppendLine(stack.Pop().Description);
            }
            throw new ImageCompilationException(sb.ToString());
        }
    }

    internal class ActivationRecord
    {
        public ActivationRecord(ActivationRecord parent, string description)
        {
            Description = description;
            if (parent != null)
            {
                this.Parent = parent;
                this.CurrentBrush = parent.CurrentBrush;
                Graphics = parent.Graphics;
                foreach (var item in parent.Variables) { this.Variables.Add(item.Key, item.Value); }
                foreach (var item in parent.VariableRecursionContexts) { this.VariableRecursionContexts.Add(item.Key, item.Value); }
            }
        }

        public ActivationRecord Parent;
        public string Description;
        public Dictionary<string, I.NestedShape> Variables = new Dictionary<string, IML.Shared.NestedShape>();
        public Brush CurrentBrush;
        public RectangleF AvailableSpace;
        public Graphics Graphics;
        public Dictionary<string, RecursionToken> VariableRecursionContexts = new Dictionary<string, RecursionToken>();

        
        public class RecursionToken
        {
            public RecursionToken(int depth) 
            { 
                DeclaredDepth = CurrentDepth = depth; 
                CurrentDepth--; 
            }
            public int DeclaredDepth, CurrentDepth;
            public float Width, Height;
        }
    }


    public class ImageCompilationException : Exception
    { 
        public ImageCompilationException(string Message) : base(Message){}
    }

    internal static class Converters
    {
        internal static Brush ToBrush(this I.Brush brushConfig)
        {
            if (!brushConfig.FillColor.IsEmpty) return new SolidBrush(brushConfig.FillColor);
            else return new SolidBrush(Color.FromArgb(brushConfig.R, brushConfig.G, brushConfig.B));
        }
    }
}
